package com.example.demo.controller;

import org.redisson.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
public class HelloController {
    private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    @Qualifier("SingleRedissonClient")
    private RedissonClient singleRedissonClient;
    @Autowired
    @Qualifier("ClusterRedissonClient")
    private RedissonClient clusterRedissonClient;

    @GetMapping("/hello/{key}/{value}")
    public Boolean hello(@PathVariable String key, @PathVariable String value) {
        redisTemplate.opsForValue().set(key, value);
        return Objects.equals(redisTemplate.opsForValue().get(key), value);
    }

    @GetMapping("/lock")
    public void lock() {
        // 1. 生成一个随机字符串
        String val = UUID.randomUUID().toString();
        // 2. 将锁状态放入redis（set if not exist），并设置锁的过期时间
        // 方式一：分两步非原子操作，存在产生死锁的风险，不推荐
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lockKey", val);
        redisTemplate.expire("lockKey", 10, TimeUnit.SECONDS);
        // 方式二：SpringBoot2 已支持直接使用 setIfAbsent() 同时设置过期时间，推荐
        lock = redisTemplate.opsForValue().setIfAbsent("lock_key", val, 10, TimeUnit.SECONDS);
        if (lock != null && lock) {
            LOGGER.info("我拿到锁啦！");
            /* 3. 之后可以执行某些任务 */
        } else {
            LOGGER.info("锁被别人抢了");
        }
        // 4. 主动释放锁
        // 方式一：没有校验，容易误删其他线程加的锁，不推荐
        redisTemplate.delete("lock_key");
        // 方式二：基于LUA脚本实现校验和删除的原子操作
        StringBuilder unlockLua = new StringBuilder();
        unlockLua.append("if redis.call('get', KEYS[1]) == ARGV[1] then ");
        unlockLua.append("    redis.call('del', KEYS[1]) ");
        unlockLua.append("end ");
        unlockLua.append("return 'OK'");
        RedisScript redisScript = RedisScript.of(String.valueOf(unlockLua));
        redisTemplate.execute(redisScript, Collections.singletonList("lock_key"), val);
    }

    /**
     * Redis单节点模式，通过Redisson存储键值对
     */
    @GetMapping("setCache/redisson/single/{key}/{value}")
    public Boolean setCacheBySingleRedissonClient(@PathVariable String key, @PathVariable String value) {
        // 通用对象桶，可以用来存放任类型的对象
        RBucket<String> bucket = singleRedissonClient.getBucket(key);
        bucket.set(value);
        System.out.println(bucket.get());
        bucket.delete();

        // RList实现了java.util.List接口
        RList<String> list = singleRedissonClient.getList(key);
        list.add("a");
        list.add("b");
        list.remove(0);
        System.out.println(list);
        list.delete();

        // RMap实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口
        RMap<String, String> map = singleRedissonClient.getMap(key);
        map.put("name", "frankie");
        map.put("age", "18");
        map.forEach((k, v) -> {
            System.out.println("key=" + k + ",value=" + v);
        });
        map.delete();

        return true;
    }

    /**
     * Redis集群模式，通过Redisson存储键值对
     */
    @GetMapping("setCache/redisson/cluster/{key}/{value}")
    public Boolean setCacheByClusterRedissonClient(@PathVariable String key, @PathVariable String value) {
        RBucket<String> bucket = clusterRedissonClient.getBucket(key);
        bucket.set(value);
        return Objects.equals(bucket.get(), value);
    }

    /**
     * 在集群上，使用 setIfAbsent() 加分布式锁，可靠性不如 Redisson
     * 如果负责储存这个分布式锁的节点宕机以后，而且这个锁正好处于锁住的状态时，这个锁会出现锁死的状态
     * Redisson 内部提供了一个监控锁的看门狗，可以避免这种情况发生
     */
    @GetMapping("/redisson_lock")
    public String redisson_lock() {
        RLock lock = clusterRedissonClient.getLock("lock_key");
        String threadName = Thread.currentThread().getName();
        try {
            LOGGER.info(threadName + " isLocked()：" + lock.isLocked());
            // 尝试在 2s 内锁住，并设置锁的过期时间为 10s
            if (lock.tryLock(2, 10, TimeUnit.SECONDS)) {
                // 能进来说明成功加锁
                LOGGER.info(threadName + " tryLock() success");
                // 接下来可以执行指定任务
                RAtomicLong rAtomicLong = clusterRedissonClient.getAtomicLong("total_number");
                long balance = rAtomicLong.decrementAndGet();
                if (balance < 0) {
                    LOGGER.info(threadName + "秒杀失败，商品库存不足。");
                } else {
                    // 生成订单
                    RAtomicLong rAtomicLong1 = clusterRedissonClient.getAtomicLong("order");
                    rAtomicLong1.incrementAndGetAsync();
                    LOGGER.info(threadName + " 秒杀成功");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 也可以在这里释放锁
            // 但要记得，如果主动释放锁时，当前线程锁已经过期失效，那么unlock()会抛异常
            try {
                lock.unlock();
                LOGGER.info(threadName + " unlock() success");
            } catch (IllegalMonitorStateException e) {
                LOGGER.info("unlock() error: " + e);
            }
        }
        return "OK";
    }
}
